package com.gofaraway.common.exception.v2;

import com.gofaraway.common.domin.ResultDTO;
import com.gofaraway.common.util.RequestUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 全局异常处理类
 *
 * @author ChenPeng
 * @Date 2024/7/24 16:16
 */
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

    private final HttpServletRequest request;

    /**
     * 统一处理参数校验异常
     * <pre>
     * 对象参数接收请求体校验不通过会抛出 MethodArgumentNotValidException
     * 普通参数校验校验不通过会抛出 ConstraintViolationException
     * 必填参数没传校验不通过会抛出 ServletRequestBindingException
     * 请求参数绑定到对象上校验不通过会抛出 BindException
     * </pre>
     */
    @ExceptionHandler({ConstraintViolationException.class,
            MethodArgumentNotValidException.class,
            ServletRequestBindingException.class,
            BindException.class})
    public ResultDTO<?> handleValidationException(Exception e) {
        String logMsg = getErrorLogMsg(e);
        String msg = "";
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException t = (MethodArgumentNotValidException) e;
            msg = getResultDefaultMsg(t.getBindingResult());
        } else if (e instanceof BindException) {
            BindException t = (BindException) e;
            msg = getBindingResultMsg(t.getBindingResult());
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException t = (ConstraintViolationException) e;
            msg = t.getConstraintViolations().stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(","));
        } else if (e instanceof MissingServletRequestParameterException) {
            MissingServletRequestParameterException t = (MissingServletRequestParameterException) e;
            msg = t.getParameterName() + " 不能为空";
        } else if (e instanceof MissingPathVariableException) {
            MissingPathVariableException t = (MissingPathVariableException) e;
            msg = t.getVariableName() + " 不能为空";
        } else {
            // 其他类型的错误当成未知异常处理
            return handleUnknownException(e);
        }
        log.warn("参数校验不通过, {}, msg: {}", logMsg, msg);
        BaseErrorInfoImpl info = new BaseErrorInfoImpl(CommonErrorConstants.BIZ_PARAM_ILLEGAL.getErrorInfo());
        info.setErrorMessagePlaceholders(Collections.singletonMap("0", " " + msg));
        return ResultDTO.ofError(info);
    }

    /**
     * 统一处理已知异常
     */
    @ExceptionHandler(BaseException.class)
    public ResultDTO<Object> handleBusinessException(BaseException baseException) {
        String logMsg = getErrorLogMsg(baseException);
        if (baseException instanceof BusinessException) {
            log.error("捕获到业务异常: {}, msg: {}", logMsg, baseException.getMessage());
        } else {
            log.error("捕获到系统异常 " + logMsg, baseException);
        }
        return ResultDTO.ofError(baseException.getBaseErrorInfo());
    }

    /**
     * 统一处理未知异常
     */
    @ExceptionHandler
    public ResultDTO<Object> handleUnknownException(Throwable t) {
        String logMsg = getErrorLogMsg(t);
        // 未知异常
        log.error("捕获到未经处理的未知异常, {}", logMsg, t);
        if (t instanceof BaseException) {
            BaseException baseException = (BaseException) t;
            return ResultDTO.ofError(baseException.getBaseErrorInfo());
        }
        return ResultDTO.ofError(CommonErrorConstants.SYS_UNKNOWN_ERROR.getErrorInfo());
    }

    private String getBindingResultMsg(BindingResult result) {
        return result.getAllErrors().stream()
                .map(e -> {
                    if (e instanceof FieldError) {
                        FieldError fieldError = (FieldError) e;
                        String field = fieldError.getField();
                        Object rejectedValue = fieldError.getRejectedValue();
                        return "字段[" + field + "]格式错误: " + rejectedValue;
                    }
                    return e.getDefaultMessage();
                })
                .collect(Collectors.joining(","));
    }

    private String getResultDefaultMsg(BindingResult result) {
        return result.getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(","));
    }

    /**
     * 异常信息应包含 url + queryString(若有) + 请求参数(这里只能拿到表单提交的参数) + username(若有)
     */
    private String getErrorLogMsg(Throwable t) {
        StringBuilder errorLogMsg = new StringBuilder();

        // url，包括查询 queryString
        errorLogMsg.append("url: ").append(RequestUtils.getFullRequestUrl(request));
        errorLogMsg.append(", ip: ").append(RequestUtils.getIpAddress(request));
        // 获取参数，这里只能拿到查询参数和以表单形式提交的参数，requestBody 的拿不到
        Map<String, String[]> params = request.getParameterMap();
        if (params != null && !params.isEmpty()) {
            StringBuilder builder = new StringBuilder();
            params.forEach((k, v) -> builder.append(",").append(k).append("=").append(Arrays.toString(v)));
            errorLogMsg.append(", params:").append(builder.substring(1));
        }
        // todo 如果能获取到当前登录人信息，则添加到最前面
        return errorLogMsg.toString();
    }


}
